﻿// Copyright (c) 2012 Mokah Nnamezie & Andrew Cooper
// Neo4Net graph database - http://neo4net.codeplex.com
//
// This file is part of Neo4Net - a .Net port of the Neo4j graph database
//
// Neo4Net is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

namespace Neo4Net.Kernel.Impl.Core
{
    using System;
    using System.IO;

    public partial class JumpingFileSystemAbstraction
    {
        public class JumpingFileStream : FileStream
        {
            private readonly int recordSize;
            private readonly JumpingFileSystemAbstraction fileSystem;
            private readonly int sizePerJump;

            public JumpingFileStream(string fileName, FileMode mode, FileAccess access, JumpingFileSystemAbstraction fileSystem, int recordSize)
                : base(fileName, mode, access)
            {
                this.recordSize = recordSize;
                this.fileSystem = fileSystem;
                this.sizePerJump = fileSystem.sizePerJump;
            }

            private long TranslateIncoming(long position)
            {
                return TranslateIncoming(position, false);
            }

            private long TranslateIncoming(long position, bool allowFix)
            {
                long actualRecord = position / recordSize;
                if (actualRecord < sizePerJump / 2)
                {
                    return position;
                }
                else
                {
                    long jumpIndex = (actualRecord + sizePerJump) / 0x100000000L;
                    long diff = actualRecord - jumpIndex * 0x100000000L;
                    diff = AssertWithinDiff(diff, allowFix);
                    long offsettedRecord = jumpIndex * sizePerJump + diff;
                    return offsettedRecord * recordSize;
                }
            }

            private long TranslateOutgoing(long offsettedPosition)
            {
                long offsettedRecord = offsettedPosition / recordSize;
                if (offsettedRecord < sizePerJump / 2)
                {
                    return offsettedPosition;
                }
                else
                {
                    long jumpIndex = (offsettedRecord - sizePerJump / 2) / sizePerJump + 1;
                    long diff = ((offsettedRecord - sizePerJump / 2) % sizePerJump) - sizePerJump / 2;
                    AssertWithinDiff(diff, false);
                    long actualRecord = jumpIndex * 0x100000000L - sizePerJump / 2 + diff;
                    return actualRecord * recordSize;
                }
            }

            private long AssertWithinDiff(long diff, bool allowFix)
            {
                if (diff < -sizePerJump / 2 || diff > sizePerJump / 2)
                {
                    if (allowFix)
                    {
                        // This is needed for shutdown() to work, PropertyStore
                        // gives an invalid offset for truncate.
                        if (diff < -sizePerJump / 2)
                        {
                            return -sizePerJump / 2;
                        }
                        else
                        {
                            return sizePerJump / 2;
                        }
                    }
                    throw new ArgumentException(diff.ToString());
                }
                return diff;
            }

            public long InternalPosition
            {
                get { return base.Position; }
            }

            public int Read(byte[] dst)
            {
                return base.Read(dst, 0, dst.Length);
            }

            public int Write(byte[] src)
            {
                base.Write(src, 0, src.Length);
                return src.Length;
            }

            public override long Position
            {
                get { return TranslateOutgoing(base.Position); }
                set { base.Position = TranslateIncoming(value); }
            }

            public Stream SetPosition(long newPosition)
            {
                Position = newPosition;
                return this;
            }

            public override long Length
            {
                get { return TranslateOutgoing(base.Length); }
            }

            public FileStream Truncate(long size)
            {
                base.SetLength(TranslateIncoming(size, true));
                return this;
            }

            public int Read(byte[] dst, long position)
            {
                var savedPosition = Position;
                Position = position;
                var bytesRead = Read(dst);
                Position = savedPosition;
                return bytesRead;
            }

            public int Write(byte[] src, long position)
            {
                var savedPosition = Position;
                Position = position;
                var bytesWritten = Write(src);
                Position = savedPosition;
                return bytesWritten;
            }

            public void Lock(long position, long size, bool shared)
            {
                base.Lock(TranslateIncoming(position), size);
            }

            protected void ImplCloseChannel()
            {
                base.Close();
            }
        }
    }
}
